<!DOCTYPE html>
<!--

  @author marcosalmeida@google.com (Marcos Almeida)
-->
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.

Use of this source code is governed by an Apache 2.0 License.
See the COPYING file for details.
-->
<head>
<title>Closure Unit Tests - goog.editor.plugins.AbstractDialogPlugin</title>
<script src="../../base.js"></script>
<script>
  goog.require('goog.dom.SavedRange');
  goog.require('goog.editor.Field');
  goog.require('goog.editor.Field.EventType');
  goog.require('goog.editor.plugins.AbstractDialogPlugin');
  goog.require('goog.editor.plugins.AbstractDialogPlugin.EventType');
  goog.require('goog.events.EventHandler');
  goog.require('goog.functions');
  goog.require('goog.testing.LooseMock');
  goog.require('goog.testing.MockClock');
  goog.require('goog.testing.MockControl');
  goog.require('goog.testing.PropertyReplacer');
  goog.require('goog.testing.editor.FieldMock');
  goog.require('goog.testing.editor.TestHelper');
  goog.require('goog.testing.events');
  goog.require('goog.testing.jsunit');
  goog.require('goog.ui.editor.AbstractDialog');
  goog.require('goog.ui.editor.AbstractDialog.Builder');
  goog.require('goog.userAgent');

</script>
</head>
<body>
<script>

  var plugin;
  var mockCtrl;
  var mockField;
  var mockSavedRange;
  var mockOpenedHandler;
  var mockClosedHandler;

  var COMMAND = 'myCommand';
  var stubs = new goog.testing.PropertyReplacer();

  var mockClock;
  var fieldObj;
  var fieldElem;
  var mockHandler;

  function setUp() {
    mockCtrl = new goog.testing.MockControl();
    mockOpenedHandler = mockCtrl.createLooseMock(goog.events.EventHandler);
    mockClosedHandler = mockCtrl.createLooseMock(goog.events.EventHandler);

    mockField = new goog.testing.editor.FieldMock(undefined, undefined, {});
    mockCtrl.addMock(mockField);
    mockField.focus();

    plugin = createDialogPlugin();
  }

  function setUpMockRange() {
    mockSavedRange = mockCtrl.createLooseMock(goog.dom.SavedRange);
    mockSavedRange.restore();

    stubs.set(goog.editor.range, 'saveUsingNormalizedCarets',
        goog.functions.constant(mockSavedRange));
  }

  function tearDown() {
    stubs.reset();
    tearDownRealEditableField();
    if (mockClock) {
      // Crucial to letting time operations work normally in the rest of tests.
      mockClock.dispose();
    }
    if (plugin) {
      mockField.$setIgnoreUnexpectedCalls(true);
      plugin.dispose();
    }
  }

  /**
   * Creates a concrete instance of goog.ui.editor.AbstractDialog by adding
   * a plain implementation of createDialogControl().
   * @param {goog.dom.DomHelper} dialogDomHelper The dom helper to be used to
   *     create the dialog.
   * @return {goog.ui.editor.AbstractDialog} The created dialog.
   */
  function createDialog(domHelper) {
    var dialog = new goog.ui.editor.AbstractDialog(domHelper);
    dialog.createDialogControl = function() {
      return new goog.ui.editor.AbstractDialog.Builder(dialog).build();
    };
    return dialog;
  }

  /**
   * Creates a concrete instance of the abstract class
   * goog.editor.plugins.AbstractDialogPlugin
   * and registers it with the mock editable field being used.
   * @return {goog.editor.plugins.AbstractDialogPlugin} The created plugin.
   */
  function createDialogPlugin() {
    var plugin = new goog.editor.plugins.AbstractDialogPlugin(COMMAND);
    plugin.createDialog = createDialog;
    plugin.returnControlToEditableField = plugin.restoreOriginalSelection;
    plugin.registerFieldObject(mockField);
    plugin.addEventListener(
        goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED,
        mockOpenedHandler);
    plugin.addEventListener(
        goog.editor.plugins.AbstractDialogPlugin.EventType.CLOSED,
        mockClosedHandler);
    return plugin;
  }

  /**
   * Sets up the mock event handler to expect an OPENED event.
   */
  function expectOpened(opt_times) {
    mockOpenedHandler.handleEvent(new goog.testing.mockmatchers.ArgumentMatcher(
        function(arg) {
          return arg.type ==
                 goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED;
        }));
    mockField.dispatchSelectionChangeEvent();
    if (opt_times) {
      mockOpenedHandler.$times(opt_times);
      mockField.$times(opt_times);
    }
  }

  /**
   * Sets up the mock event handler to expect a CLOSED event.
   */
  function expectClosed(opt_times) {
    mockClosedHandler.handleEvent(new goog.testing.mockmatchers.ArgumentMatcher(
        function(arg) {
          return arg.type ==
                 goog.editor.plugins.AbstractDialogPlugin.EventType.CLOSED;
        }));
    mockField.dispatchSelectionChangeEvent();
    if (opt_times) {
      mockClosedHandler.$times(opt_times);
      mockField.$times(opt_times);
    }
  }


  /**
   * Tests the simple flow of calling execCommand (which opens the
   * dialog) and immediately disposing of the plugin (which closes the dialog).
   * @param {boolean} opt_reuse Whether to set the plugin to reuse its dialog.
   */
  function testExecAndDispose(opt_reuse) {
    setUpMockRange();
    expectOpened();
    expectClosed();
    mockField.debounceEvent(goog.editor.Field.EventType.SELECTIONCHANGE);
    mockCtrl.$replayAll();
    if (opt_reuse) {
      plugin.setReuseDialog(true);
    }
    assertFalse('Dialog should not be open yet',
                !!plugin.getDialog() && plugin.getDialog().isOpen());

    plugin.execCommand(COMMAND);
    assertTrue('Dialog should be open now',
               !!plugin.getDialog() && plugin.getDialog().isOpen());

    var tempDialog = plugin.getDialog();
    plugin.dispose();
    assertFalse('Dialog should not still be open after disposal',
                tempDialog.isOpen());
    mockCtrl.$verifyAll();
  }

  /**
   * Tests execCommand and dispose while reusing the dialog.
   */
  function testExecAndDisposeReuse() {
    testExecAndDispose(true);
  }


  /**
   * Tests the flow of calling execCommand (which opens the dialog) and
   * then hiding it (simulating that a user did somthing to cause the dialog to
   * close).
   * @param {boolean} reuse Whether to set the plugin to reuse its dialog.
   */
  function testExecAndHide(opt_reuse) {
    setUpMockRange();
    expectOpened();
    expectClosed();
    mockField.debounceEvent(goog.editor.Field.EventType.SELECTIONCHANGE);
    mockCtrl.$replayAll();
    if (opt_reuse) {
      plugin.setReuseDialog(true);
    }
    assertFalse('Dialog should not be open yet',
                !!plugin.getDialog() && plugin.getDialog().isOpen());

    plugin.execCommand(COMMAND);
    assertTrue('Dialog should be open now',
               !!plugin.getDialog() && plugin.getDialog().isOpen());

    var tempDialog = plugin.getDialog();
    plugin.getDialog().hide();
    assertFalse('Dialog should not still be open after hiding',
                tempDialog.isOpen());
    if (opt_reuse) {
      assertFalse('Dialog should not be disposed after hiding (will be reused)',
                  tempDialog.isDisposed());
    } else {
      assertTrue('Dialog should be disposed after hiding',
                 tempDialog.isDisposed());
    }
    plugin.dispose();
    mockCtrl.$verifyAll();
  }

  /**
   * Tests execCommand and hide while reusing the dialog.
   */
  function testExecAndHideReuse() {
    testExecAndHide(true);
  }

  /**
   * Tests the flow of calling execCommand (which opens a dialog) and
   * then calling it again before the first dialog is closed. This is not
   * something anyone should be doing since dialogs are (usually?) modal so the
   * user can't do another execCommand before closing the first dialog. But
   * since the API makes it possible, I thought it would be good to guard
   * against and unit test.
   * @param {boolean} reuse Whether to set the plugin to reuse its dialog.
   */
  function testExecTwice(opt_reuse) {
    setUpMockRange();
    if (opt_reuse) {
      expectOpened(2); // The second exec should cause a second OPENED event.
      // But the dialog was not closed between exec calls, so only one CLOSED is
      // expected.
      expectClosed();
      plugin.setReuseDialog(true);
      mockField.debounceEvent(goog.editor.Field.EventType.SELECTIONCHANGE);
    } else {
      expectOpened(2); // The second exec should cause a second OPENED event.
      // The first dialog will be disposed so there should be two CLOSED events.
      expectClosed(2);
      mockSavedRange.restore(); // Expected 2x, once already recorded in setup.
      mockField.focus(); // Expected 2x, once already recorded in setup.
      mockField.debounceEvent(goog.editor.Field.EventType.SELECTIONCHANGE);
      mockField.$times(2);
    }
    mockCtrl.$replayAll();

    assertFalse('Dialog should not be open yet',
                !!plugin.getDialog() && plugin.getDialog().isOpen());

    plugin.execCommand(COMMAND);
    assertTrue('Dialog should be open now',
               !!plugin.getDialog() && plugin.getDialog().isOpen());

    var tempDialog = plugin.getDialog();
    plugin.execCommand(COMMAND);
    if (opt_reuse) {
      assertTrue('Reused dialog should still be open after second exec',
                 tempDialog.isOpen());
      assertFalse('Reused dialog should not be disposed after second exec',
                 tempDialog.isDisposed());
    } else {
      assertFalse('First dialog should not still be open after opening second',
                  tempDialog.isOpen());
      assertTrue('First dialog should be disposed after opening second',
                 tempDialog.isDisposed());
    }
    plugin.dispose();
    mockCtrl.$verifyAll();
  }

  /**
   * Tests execCommand twice while reusing the dialog.
   */
  function testExecTwiceReuse() {
    testExecTwice(true);
  }


  /**
   * Tests that the selection is cleared when the dialog opens and is
   * correctly restored after it closes.
   */
  function testRestoreSelection() {
    setUpRealEditableField();

    fieldObj.setHtml(false, '12345');
    var elem = fieldObj.getElement();
    var helper = new goog.testing.editor.TestHelper(elem);
    helper.select('12345', 1, '12345', 4); // Selects '234'.

    assertEquals('Incorrect text selected before dialog is opened',
                 '234',
                 fieldObj.getRange().getText());
    plugin.execCommand(COMMAND);
    if (!goog.userAgent.IE && !goog.userAgent.OPERA) {
      // IE returns some bogus range when field doesn't have selection.
      // Opera can't remove the selection from a whitebox field.
      assertNull('There should be no selection while dialog is open',
                 fieldObj.getRange());
    }
    plugin.getDialog().hide();
    assertEquals('Incorrect text selected after dialog is closed',
                 '234',
                 fieldObj.getRange().getText());
  }

  /**
   * Setup a real editable field (instead of a mock) and register the plugin to
   * it.
   */
  function setUpRealEditableField() {
    fieldElem = document.createElement('div');
    fieldElem.id = 'myField';
    document.body.appendChild(fieldElem);
    fieldObj = new goog.editor.Field('myField', document);
    fieldObj.makeEditable();
    // Register the plugin to that field.
    plugin.getTrogClassId = goog.functions.constant('myClassId');
    fieldObj.registerPlugin(plugin);
  }

  /**
   * Tear down the real editable field.
   */
  function tearDownRealEditableField() {
    if (fieldObj) {
      fieldObj.makeUneditable();
      fieldObj.dispose();
      fieldObj = null;
    }
    if (fieldElem && fieldElem.parentNode == document.body) {
      document.body.removeChild(fieldElem);
    }
  }


  /**
   * Tests that after the dialog is hidden via a keystroke, the editable field
   * doesn't fire an extra SELECTIONCHANGE event due to the keyup from that
   * keystroke.
   * There is also a robot test in dialog_robot.html to test debouncing the
   * SELECTIONCHANGE event when the dialog closes.
   */
  function testDebounceSelectionChange() {
    mockClock = new goog.testing.MockClock(true);
    // Initial time is 0 which evaluates to false in debouncing implementation.
    mockClock.tick(1);

    setUpRealEditableField();

    // Set up a mock event handler to make sure selection change isn't fired
    // more than once on close and a second time on close.
    var count = 0;
    fieldObj.addEventListener(goog.editor.Field.EventType.SELECTIONCHANGE,
                              function(e) {
                                count++;
                              });

    assertEquals(0, count);
    plugin.execCommand(COMMAND);
    assertEquals(1, count);
    plugin.getDialog().hide();
    assertEquals(2, count);

    // Fake the keyup event firing on the field after the dialog closes.
    var e = new goog.events.Event('keyup', plugin.fieldObject.getElement());
    e.keyCode = 13;
    goog.testing.events.fireBrowserEvent(e);

    // Tick the mock clock so that selection change tries to fire.
    mockClock.tick(goog.editor.Field.SELECTION_CHANGE_FREQUENCY_ + 1);

    // Ensure the handler did not fire again.
    assertEquals(2, count);
  }

</script>
</body>
</html>
